import datetime
import logging
from unittest.mock import Mock, patch

import pghistory
from auditlog.context import set_actor
from crum import impersonate
from django.test import override_settings
from django.urls import reverse
from django.utils import timezone
from rest_framework.authtoken.models import Token
from rest_framework.test import APIClient, APITestCase

from dojo import __version__ as dd_version
from dojo.importers.base_importer import BaseImporter
from dojo.models import (
    DEFAULT_NOTIFICATION,
    Alerts,
    Development_Environment,
    Dojo_User,
    Endpoint,
    Engagement,
    Finding,
    Finding_Group,
    Notification_Webhooks,
    Notifications,
    Product,
    Product_Type,
    System_Settings,
    Test,
    Test_Type,
    User,
    get_current_datetime,
)
from dojo.notifications.helper import (
    AlertNotificationManger,
    WebhookNotificationManger,
    create_notification,
    webhook_status_cleanup,
)

from .dojo_test_case import DojoTestCase

logger = logging.getLogger(__name__)


class TestNotifications(DojoTestCase):
    fixtures = ["dojo_testdata.json"]

    def test_merge_notifications_list(self):
        global_personal_notifications = Notifications(user=User.objects.get(username="admin"))
        personal_product_notifications = Notifications(user=User.objects.get(username="admin"), product=Product.objects.all()[0])

        global_personal_notifications.product_added = ["alert"]
        global_personal_notifications.test_added = ""
        global_personal_notifications.scan_added = None
        global_personal_notifications.other = ["slack", "mail"]

        global_personal_notifications.save()  # we have to save it and retrieve it because only then the fields get turned into lists...

        global_personal_notifications = Notifications.objects.get(id=global_personal_notifications.id)

        personal_product_notifications.product_added = ["mail"]
        personal_product_notifications.test_added = ["mail", "alert"]
        personal_product_notifications.scan_added = None

        personal_product_notifications.save()

        personal_product_notifications = Notifications.objects.get(id=personal_product_notifications.id)

        merged_notifications = Notifications.merge_notifications_list([global_personal_notifications, personal_product_notifications])

        self.assertEqual("alert" in merged_notifications.product_added, True)
        self.assertEqual("mail" in merged_notifications.product_added, True)
        self.assertEqual("slack" in merged_notifications.product_added, False)
        self.assertEqual(len(merged_notifications.product_added), 2)

        self.assertEqual("alert" in merged_notifications.test_added, True)
        self.assertEqual("mail" in merged_notifications.test_added, True)
        self.assertEqual("slack" in merged_notifications.test_added, False)
        self.assertEqual(len(merged_notifications.test_added), 2)

        self.assertEqual("alert" in merged_notifications.scan_added, False)
        self.assertEqual("mail" in merged_notifications.scan_added, False)
        self.assertEqual("slack" in merged_notifications.scan_added, False)
        self.assertEqual(len(merged_notifications.scan_added), 0)

        self.assertEqual("alert" in merged_notifications.other, True)
        self.assertEqual("mail" in merged_notifications.other, True)
        self.assertEqual("slack" in merged_notifications.other, True)  # default alert from global
        self.assertEqual(len(merged_notifications.other), 3)
        self.assertEqual(merged_notifications.other, {"alert", "mail", "slack"})

    # @patch("dojo.notifications.helper.AlertNotificationManger.send_alert_notification", wraps=AlertNotificationManger.send_alert_notification)
    @patch("dojo.notifications.helper.NotificationManager._get_manager_instance")
    def test_notifications_system_level_trump(self, mock_get_manager_instance):
        mock_manager = Mock(wraps=AlertNotificationManger())
        mock_get_manager_instance.return_value = mock_manager

        notif_user, _ = Notifications.objects.get_or_create(user=User.objects.get(username="admin"))
        notif_system, _ = Notifications.objects.get_or_create(user=None, template=False)

        last_count = mock_manager.send_alert_notification.call_count
        with self.subTest("user off, system off"):
            notif_user.user_mentioned = ()  # no alert
            notif_user.save()
            notif_system.user_mentioned = ()  # no alert
            notif_system.save()
            create_notification(event="user_mentioned", title="user_mentioned", recipients=["admin"])
            self.assertEqual(mock_manager.send_alert_notification.call_count, last_count)

        last_count = mock_manager.send_alert_notification.call_count
        with self.subTest("user off, system on"):
            notif_user.user_mentioned = ()  # no alert
            notif_user.save()
            notif_system.user_mentioned = DEFAULT_NOTIFICATION  # alert only
            notif_system.save()
            create_notification(event="user_mentioned", title="user_mentioned", recipients=["admin"])
            self.assertEqual(mock_manager.send_alert_notification.call_count, last_count + 1)

        # Small note for this test-cast: Trump works only in positive direction - system is not able to disable some kind of notification if user enabled it
        last_count = mock_manager.send_alert_notification.call_count
        with self.subTest("user on, system off"):
            notif_user.user_mentioned = DEFAULT_NOTIFICATION  # alert only
            notif_user.save()
            notif_system.user_mentioned = ()  # no alert
            notif_system.save()
            create_notification(event="user_mentioned", title="user_mentioned", recipients=["admin"])
            self.assertEqual(mock_manager.send_alert_notification.call_count, last_count + 1)

        last_count = mock_manager.send_alert_notification.call_count
        with self.subTest("user on, system on"):
            notif_user.user_mentioned = DEFAULT_NOTIFICATION  # alert only
            notif_user.save()
            notif_system.user_mentioned = DEFAULT_NOTIFICATION  # alert only
            notif_system.save()
            create_notification(event="user_mentioned", title="user_mentioned", recipients=["admin"])
            self.assertEqual(mock_manager.send_alert_notification.call_count, last_count + 1)
        last_count = mock_manager.send_alert_notification.call_count

    # @patch("dojo.notifications.helper.AlertNotificationManger.send_alert_notification", wraps=AlertNotificationManger.send_alert_notification)
    @patch("dojo.notifications.helper.NotificationManager._get_manager_instance")
    def test_non_default_other_notifications(self, mock_get_manager_instance):
        mock_manager = Mock(wraps=AlertNotificationManger())
        mock_get_manager_instance.return_value = mock_manager

        notif_user, _ = Notifications.objects.get_or_create(user=User.objects.get(username="admin"))
        notif_system, _ = Notifications.objects.get_or_create(user=None, template=False)

        last_count = mock_manager.send_alert_notification.call_count
        with self.subTest("do not notify other"):
            notif_user.other = ()  # no alert
            notif_user.save()
            create_notification(event="dummy_bar_event", recipients=["admin"])
            self.assertEqual(mock_manager.send_alert_notification.call_count, last_count)

        last_count = mock_manager.send_alert_notification.call_count
        with self.subTest("notify other"):
            notif_user.other = DEFAULT_NOTIFICATION  # alert only
            notif_user.save()
            create_notification(event="dummy_foo_event", title="title_for_dummy_foo_event", description="description_for_dummy_foo_event", recipients=["admin"])
            self.assertEqual(mock_manager.send_alert_notification.call_count, last_count + 1)
            self.assertEqual(mock_manager.send_alert_notification.call_args_list[0].args[0], "dummy_foo_event")
            alert = Alerts.objects.get(title="title_for_dummy_foo_event")
            self.assertEqual(alert.source, "Dummy Foo Event")

        last_count = mock_manager.send_alert_notification.call_count
        with self.subTest("user off, system off"):
            notif_user.user_mentioned = ()  # no alert
            notif_user.save()
            notif_system.user_mentioned = ()  # no alert
            notif_system.save()
            create_notification(event="user_mentioned", title="user_mentioned", recipients=["admin"])
            self.assertEqual(mock_manager.send_alert_notification.call_count, last_count + 0)

        last_count = mock_manager.send_alert_notification.call_count
        with self.subTest("user off, system on"):
            notif_user.user_mentioned = ()  # no alert
            notif_user.save()
            notif_system.user_mentioned = DEFAULT_NOTIFICATION  # alert only
            notif_system.save()
            create_notification(event="user_mentioned", title="user_mentioned", recipients=["admin"])
            self.assertEqual(mock_manager.send_alert_notification.call_count, last_count + 1)

        # Small note for this test-cast: Trump works only in positive direction - system is not able to disable some kind of notification if user enabled it
        last_count = mock_manager.send_alert_notification.call_count
        with self.subTest("user on, system off"):
            notif_user.user_mentioned = DEFAULT_NOTIFICATION  # alert only
            notif_user.save()
            notif_system.user_mentioned = ()  # no alert
            notif_system.save()
            create_notification(event="user_mentioned", title="user_mentioned", recipients=["admin"])
            self.assertEqual(mock_manager.send_alert_notification.call_count, last_count + 1)

        last_count = mock_manager.send_alert_notification.call_count
        with self.subTest("user on, system on"):
            notif_user.user_mentioned = DEFAULT_NOTIFICATION  # alert only
            notif_user.save()
            notif_system.user_mentioned = DEFAULT_NOTIFICATION  # alert only
            notif_system.save()
            create_notification(event="user_mentioned", title="user_mentioned", recipients=["admin"])
            self.assertEqual(mock_manager.send_alert_notification.call_count, last_count + 1)


class TestNotificationTriggers(DojoTestCase):
    fixtures = ["dojo_testdata.json"]

    def setUp(self):
        self.notification_tester = Dojo_User.objects.get(username="admin")

    @patch("dojo.notifications.helper.NotificationManager._process_notifications")
    def test_product_types(self, mock):

        last_count = mock.call_count
        with self.subTest("product_type_added"):
            with set_actor(self.notification_tester), pghistory.context(user=self.notification_tester.id):
                prod_type = Product_Type.objects.create(name="notif prod type")
            self.assertEqual(mock.call_count, last_count + 4)
            self.assertEqual(mock.call_args_list[-1].args[0], "product_type_added")
            self.assertEqual(mock.call_args_list[-1].kwargs["url"], f"/product/type/{prod_type.id}")

        last_count = mock.call_count
        with self.subTest("product_type_deleted"):
            with set_actor(self.notification_tester), pghistory.context(user=self.notification_tester.id):
                prod_type.delete()
            self.assertEqual(mock.call_count, last_count + 1)
            self.assertEqual(mock.call_args_list[-1].args[0], "product_type_deleted")
            self.assertEqual(mock.call_args_list[-1].kwargs["description"], 'The product type "notif prod type" was deleted by admin')
            self.assertEqual(mock.call_args_list[-1].kwargs["url"], "/product/type")

    @patch("dojo.notifications.helper.NotificationManager._process_notifications")
    def test_products(self, mock):

        last_count = mock.call_count
        with self.subTest("product_added"):
            with set_actor(self.notification_tester), pghistory.context(user=self.notification_tester.id):
                prod_type = Product_Type.objects.first()
                prod, _ = Product.objects.get_or_create(prod_type=prod_type, name="prod name")
            self.assertEqual(mock.call_count, last_count + 5)
            self.assertEqual(mock.call_args_list[-1].args[0], "product_added")
            self.assertEqual(mock.call_args_list[-1].kwargs["url"], f"/product/{prod.id}")

        last_count = mock.call_count
        with self.subTest("product_deleted"):
            with set_actor(self.notification_tester), pghistory.context(user=self.notification_tester.id):
                prod.delete()
            self.assertEqual(mock.call_count, last_count + 2)
            self.assertEqual(mock.call_args_list[-1].args[0], "product_deleted")
            self.assertEqual(mock.call_args_list[-1].kwargs["description"], 'The product "prod name" was deleted by admin')
            self.assertEqual(mock.call_args_list[-1].kwargs["url"], "/product")

    @patch("dojo.notifications.helper.NotificationManager._process_notifications")
    def test_engagements(self, mock):

        last_count = mock.call_count
        with self.subTest("engagement_added"):
            with set_actor(self.notification_tester), pghistory.context(user=self.notification_tester.id):
                prod = Product.objects.first()
                eng = Engagement.objects.create(product=prod, target_start=timezone.now(), target_end=timezone.now())
            self.assertEqual(mock.call_count, last_count + 5)
            self.assertEqual(mock.call_args_list[-1].args[0], "engagement_added")
            self.assertEqual(mock.call_args_list[-1].kwargs["url"], f"/engagement/{eng.id}")

        last_count = mock.call_count
        with self.subTest("close_engagement"):
            with set_actor(self.notification_tester), pghistory.context(user=self.notification_tester.id):
                eng.status = "Completed"
                eng.save()
            self.assertEqual(mock.call_count, last_count + 5)
            self.assertEqual(mock.call_args_list[-1].args[0], "engagement_closed")
            self.assertEqual(mock.call_args_list[-1].kwargs["url"], f"/engagement/{eng.id}/finding/all")

        last_count = mock.call_count
        with self.subTest("reopen_engagement"):
            with set_actor(self.notification_tester), pghistory.context(user=self.notification_tester.id):
                eng.status = "In Progress"
                eng.save()
            self.assertEqual(mock.call_count, last_count + 5)
            self.assertEqual(mock.call_args_list[-1].args[0], "engagement_reopened")
            self.assertEqual(mock.call_args_list[-1].kwargs["url"], f"/engagement/{eng.id}")

        eng.status = "Not Started"
        eng.save()
        last_count = mock.call_count
        with self.subTest("no reopen_engagement from not started"):
            with set_actor(self.notification_tester), pghistory.context(user=self.notification_tester.id):
                eng.status = "In Progress"
                eng.save()
            self.assertEqual(mock.call_count, last_count)

        prod_type = Product_Type.objects.first()
        prod1, _ = Product.objects.get_or_create(prod_type=prod_type, name="prod name 1")
        _ = Engagement.objects.create(product=prod1, target_start=timezone.now(), target_end=timezone.now(), lead=User.objects.get(username="admin"))
        prod2, _ = Product.objects.get_or_create(prod_type=prod_type, name="prod name 2")
        eng2 = Engagement.objects.create(product=prod2, name="Testing engagement", target_start=timezone.now(), target_end=timezone.now(), lead=User.objects.get(username="admin"))

        with self.subTest("engagement_deleted by product"):  # in case of product removal, we are not notifying about removal
            with set_actor(self.notification_tester), pghistory.context(user=self.notification_tester.id):
                prod1.delete()
            for call in mock.call_args_list:
                self.assertNotEqual(call.args[0], "engagement_deleted")

        last_count = mock.call_count
        with self.subTest("engagement_deleted itself"):
            with set_actor(self.notification_tester), pghistory.context(user=self.notification_tester.id):
                eng2.delete()
            self.assertEqual(mock.call_count, last_count + 1)
            self.assertEqual(mock.call_args_list[-1].args[0], "engagement_deleted")
            self.assertEqual(mock.call_args_list[-1].kwargs["description"], 'The engagement "Testing engagement" was deleted by admin')
            self.assertEqual(mock.call_args_list[-1].kwargs["url"], f"/product/{prod2.id}")

    @patch("dojo.notifications.helper.NotificationManager._process_notifications")
    def test_endpoints(self, mock):
        prod_type = Product_Type.objects.first()
        prod1, _ = Product.objects.get_or_create(prod_type=prod_type, name="prod name 1")
        Endpoint.objects.get_or_create(product=prod1, host="host1")
        prod2, _ = Product.objects.get_or_create(prod_type=prod_type, name="prod name 2")
        endpoint2, _ = Endpoint.objects.get_or_create(product=prod2, host="host2")

        with self.subTest("endpoint_deleted by product"):  # in case of product removal, we are not notifying about removal
            with set_actor(self.notification_tester), pghistory.context(user=self.notification_tester.id):
                prod1.delete()
            for call in mock.call_args_list:
                self.assertNotEqual(call.args[0], "endpoint_deleted")

        last_count = mock.call_count
        with self.subTest("endpoint_deleted itself"):
            with set_actor(self.notification_tester), pghistory.context(user=self.notification_tester.id):
                endpoint2.delete()
            self.assertEqual(mock.call_count, last_count + 2)
            self.assertEqual(mock.call_args_list[-1].args[0], "endpoint_deleted")
            self.assertEqual(mock.call_args_list[-1].kwargs["description"], 'The endpoint "host2" was deleted by admin')
            self.assertEqual(mock.call_args_list[-1].kwargs["url"], "/endpoint")

    @patch("dojo.notifications.helper.NotificationManager._process_notifications")
    def test_tests(self, mock):
        prod_type = Product_Type.objects.first()
        prod, _ = Product.objects.get_or_create(prod_type=prod_type, name="prod name")
        eng1 = Engagement.objects.create(product=prod, target_start=timezone.now(), target_end=timezone.now(), lead=User.objects.get(username="admin"))
        Test.objects.create(engagement=eng1, target_start=timezone.now(), target_end=timezone.now(), test_type_id=Test_Type.objects.first().id)
        eng2 = Engagement.objects.create(product=prod, target_start=timezone.now(), target_end=timezone.now(), lead=User.objects.get(username="admin"))
        test2 = Test.objects.create(engagement=eng2, target_start=timezone.now(), target_end=timezone.now(), test_type_id=Test_Type.objects.first().id)

        with self.subTest("test_deleted by engagement"):  # in case of engagement removal, we are not notifying about removal
            with set_actor(self.notification_tester), pghistory.context(user=self.notification_tester.id):
                eng1.delete()
            for call in mock.call_args_list:
                self.assertNotEqual(call.args[0], "test_deleted")

        last_count = mock.call_count
        with self.subTest("test_deleted itself"):
            with set_actor(self.notification_tester), pghistory.context(user=self.notification_tester.id):
                test2.delete()
            self.assertEqual(mock.call_count, last_count + 1)
            self.assertEqual(mock.call_args_list[-1].args[0], "test_deleted")
            self.assertEqual(mock.call_args_list[-1].kwargs["description"], 'The test "Acunetix Scan" was deleted by admin')
            self.assertEqual(mock.call_args_list[-1].kwargs["url"], f"/engagement/{eng2.id}")

    @patch("dojo.notifications.helper.NotificationManager._process_notifications")
    def test_finding_groups(self, mock):
        prod_type = Product_Type.objects.first()
        prod, _ = Product.objects.get_or_create(prod_type=prod_type, name="prod name")
        eng, _ = Engagement.objects.get_or_create(product=prod, target_start=timezone.now(), target_end=timezone.now(), lead=User.objects.get(username="admin"))
        test1, _ = Test.objects.get_or_create(engagement=eng, target_start=timezone.now(), target_end=timezone.now(), test_type_id=Test_Type.objects.first().id)
        Finding_Group.objects.get_or_create(test=test1, creator=User.objects.get(username="admin"))
        test2, _ = Test.objects.get_or_create(engagement=eng, target_start=timezone.now(), target_end=timezone.now(), test_type_id=Test_Type.objects.first().id)
        fg2, _ = Finding_Group.objects.get_or_create(test=test2, name="fg test", creator=User.objects.get(username="admin"))

        with self.subTest("test_deleted by engagement"):  # in case of engagement removal, we are not notifying about removal
            with set_actor(self.notification_tester), pghistory.context(user=self.notification_tester.id):
                test1.delete()
            for call in mock.call_args_list:
                self.assertNotEqual(call.args[0], "finding_group_deleted")

        last_count = mock.call_count
        with self.subTest("test_deleted itself"):
            with set_actor(self.notification_tester), pghistory.context(user=self.notification_tester.id):
                fg2.delete()
            self.assertEqual(mock.call_count, last_count + 5)
            self.assertEqual(mock.call_args_list[-1].args[0], "finding_group_deleted")
            self.assertEqual(mock.call_args_list[-1].kwargs["description"], 'The finding group "fg test" was deleted by admin')
            self.assertEqual(mock.call_args_list[-1].kwargs["url"], f"/test/{test2.id}")

    @patch("dojo.notifications.helper.NotificationManager._process_notifications")
    @override_settings(ENABLE_AUDITLOG=True)
    def test_auditlog_on(self, mock):
        prod_type = Product_Type.objects.create(name="notif prod type")
        with set_actor(self.notification_tester), pghistory.context(user=self.notification_tester.id):
            prod_type.delete()
        self.assertEqual(mock.call_args_list[-1].kwargs["description"], 'The product type "notif prod type" was deleted by admin')

    @patch("dojo.notifications.helper.NotificationManager._process_notifications")
    @override_settings(ENABLE_AUDITLOG=False)
    def test_auditlog_off(self, mock):
        prod_type = Product_Type.objects.create(name="notif prod type")
        with set_actor(self.notification_tester), pghistory.context(user=self.notification_tester.id):
            prod_type.delete()
        self.assertEqual(mock.call_args_list[-1].kwargs["description"], 'The product type "notif prod type" was deleted')


class TestNotificationTriggersApi(APITestCase):
    fixtures = ["dojo_testdata.json"]

    def setUp(self):
        token = Token.objects.get(user__username="admin")
        self.client = APIClient()
        self.client.credentials(HTTP_AUTHORIZATION="Token " + token.key)
        self.admin = User.objects.get(username="admin")
        self.base_url = "/api/v2/findings/"

    def _minimal_create_payload(self, title: str):
        return {
            "test": 3,
            "found_by": [],
            "title": title,
            "date": "2020-05-20",
            "cwe": 1,
            "severity": "High",
            "description": "TEST finding for notification",
            "mitigation": "MITIGATION",
            "impact": "HIGH",
            "references": "",
            "active": True,
            "verified": False,
            "false_p": False,
            "duplicate": False,
            "out_of_scope": False,
            "under_review": False,
            "under_defect_review": False,
            "numerical_severity": "S0",
        }

    @patch("dojo.notifications.helper.NotificationManager._process_notifications")
    def test_auditlog_on(self, mock):
        prod_type = Product_Type.objects.create(name="notif prod type API")
        self.client.delete(reverse("product_type-detail", args=(prod_type.pk,)), format="json")
        self.assertEqual(mock.call_args_list[-1].kwargs["description"], 'The product type "notif prod type API" was deleted by admin')

    @patch("dojo.api_v2.serializers.create_notification")
    def test_create_calls_notification_with_auto_assigned_reporter(self, mock_create_notification):
        """Test that create_notification is called when creating a finding without explicit reporter."""
        payload = self._minimal_create_payload("Finding with auto-assigned reporter notification")

        response = self.client.post(self.base_url, payload, format="json")
        self.assertEqual(201, response.status_code, response.content[:1000])

        # Verify notification was called
        mock_create_notification.assert_called_once()
        call_args = mock_create_notification.call_args

        # Check the notification parameters
        self.assertEqual(call_args[1]["event"], "finding_added")
        self.assertEqual(call_args[1]["title"], "Addition of Finding With Auto-Assigned Reporter Notification")
        self.assertEqual(
            call_args[1]["description"],
            f'Finding "Finding With Auto-Assigned Reporter Notification" was added by {self.admin}',
        )
        self.assertEqual(call_args[1]["icon"], "exclamation-triangle")

        # Verify the finding was created successfully
        created_id = response.data.get("id")
        self.assertIsNotNone(created_id)
        created_finding = Finding.objects.get(id=created_id)
        self.assertEqual(created_finding.reporter, self.admin)

    @patch("dojo.api_v2.serializers.create_notification")
    def test_create_calls_notification_with_explicit_reporter(self, mock_create_notification):
        """Test that create_notification is called when creating a finding with explicit reporter."""
        # Create another user to use as explicit reporter
        explicit_reporter = User.objects.create(username="explicit_reporter", email="reporter@test.com")

        payload = self._minimal_create_payload("Finding with explicit reporter notification")
        payload["reporter"] = explicit_reporter.id

        response = self.client.post(self.base_url, payload, format="json")
        self.assertEqual(201, response.status_code, response.content[:1000])

        # Verify notification was called
        mock_create_notification.assert_called_once()
        call_args = mock_create_notification.call_args

        # Check the notification parameters
        self.assertEqual(call_args[1]["event"], "finding_added")
        self.assertEqual(call_args[1]["title"], "Addition of Finding With Explicit Reporter Notification")
        self.assertEqual(
            call_args[1]["description"],
            f'Finding "Finding With Explicit Reporter Notification" was added by {explicit_reporter}',
        )
        self.assertEqual(call_args[1]["icon"], "exclamation-triangle")

        # Verify the finding was created with explicit reporter
        created_id = response.data.get("id")
        self.assertIsNotNone(created_id)
        created_finding = Finding.objects.get(id=created_id)
        self.assertEqual(created_finding.reporter, explicit_reporter)

    @patch("dojo.api_v2.serializers.create_notification")
    def test_notification_parameters_are_correct(self, mock_create_notification):
        """Test that all notification parameters are properly formatted and passed."""
        payload = self._minimal_create_payload("Test Finding for Parameter Validation")

        response = self.client.post(self.base_url, payload, format="json")
        self.assertEqual(201, response.status_code, response.content[:1000])

        # Get the created finding to verify URL formation
        created_id = response.data.get("id")
        created_finding = Finding.objects.get(id=created_id)

        # Verify notification was called with correct parameters
        mock_create_notification.assert_called_once()
        call_args = mock_create_notification.call_args

        # Verify all required parameters exist
        self.assertEqual(call_args[1]["event"], "finding_added")
        self.assertEqual(call_args[1]["title"], "Addition of Test Finding for Parameter Validation")
        self.assertEqual(
            call_args[1]["description"],
            f'Finding "Test Finding for Parameter Validation" was added by {self.admin}',
        )
        self.assertEqual(call_args[1]["url"], f"/finding/{created_finding.id}")
        self.assertEqual(call_args[1]["icon"], "exclamation-triangle")
        self.assertEqual(call_args[1]["finding"], created_finding)


class TestNotificationWebhooks(DojoTestCase):
    fixtures = ["dojo_testdata.json"]

    def run(self, result=None):
        testuser = User.objects.get(username="admin")
        testuser.usercontactinfo.block_execution = True
        testuser.save()

        # unit tests are running without any user, which will result in actions like dedupe happening in the celery process
        # this doesn't work in unittests as unittests are using an in memory sqlite database and celery can't see the data
        # so we're running the test under the admin user context and set block_execution to True
        with impersonate(testuser):
            super().run(result)

    def setUp(self):
        self.system_settings(enable_webhooks_notifications=True)
        self.sys_wh = Notification_Webhooks.objects.filter(owner=None).first()
        self.url_base = "http://webhook.endpoint:8080"

    def test_missing_system_webhook(self):
        # test data contains 2 entries but we need to test missing definition
        Notification_Webhooks.objects.all().delete()
        with self.assertLogs("dojo.notifications.helper", level="INFO") as cm:
            manager = WebhookNotificationManger()
            manager.send_webhooks_notification(event="dummy")
        self.assertIn("URLs for Webhooks not configured: skipping system notification", cm.output[0])

    def test_missing_personal_webhook(self):
        # test data contains 2 entries but we need to test missing definition
        Notification_Webhooks.objects.all().delete()
        with self.assertLogs("dojo.notifications.helper", level="INFO") as cm:
            manager = WebhookNotificationManger()
            manager.send_webhooks_notification(event="dummy", user=Dojo_User.objects.get(username="admin"))
        self.assertIn("URLs for Webhooks not configured for user '(admin)': skipping user notification", cm.output[0])

    def test_system_webhook_inactive(self):
        self.sys_wh.status = Notification_Webhooks.Status.STATUS_INACTIVE_PERMANENT
        self.sys_wh.save()
        with self.assertLogs("dojo.notifications.helper", level="INFO") as cm:
            manager = WebhookNotificationManger()
            manager.send_webhooks_notification(event="dummy")
        self.assertIn("URL for Webhook 'My webhook endpoint' is not active: Permanently inactive (inactive_permanent)", cm.output[0])

    def test_system_webhook_sucessful(self):
        with self.assertLogs("dojo.notifications.helper", level="DEBUG") as cm:
            manager = WebhookNotificationManger()
            manager.send_webhooks_notification(event="dummy")
        self.assertIn("Message sent to endpoint 'My webhook endpoint' successfully.", cm.output[-1])

        updated_wh = Notification_Webhooks.objects.filter(owner=None).first()
        self.assertEqual(updated_wh.status, Notification_Webhooks.Status.STATUS_ACTIVE)
        self.assertIsNone(updated_wh.first_error)
        self.assertIsNone(updated_wh.last_error)

    def test_system_webhook_4xx(self):
        self.sys_wh.url = f"{self.url_base}/status/400"
        self.sys_wh.save()

        with self.assertLogs("dojo.notifications.helper", level="ERROR") as cm:
            manager = WebhookNotificationManger()
            manager.send_webhooks_notification(event="dummy", title="Dummy event")
        self.assertIn("Error when sending message to Webhooks 'My webhook endpoint' (status: 400)", cm.output[-1])

        updated_wh = Notification_Webhooks.objects.all().filter(owner=None).first()
        self.assertEqual(updated_wh.status, Notification_Webhooks.Status.STATUS_INACTIVE_PERMANENT)
        self.assertIsNotNone(updated_wh.first_error)
        self.assertEqual(updated_wh.first_error, updated_wh.last_error)

    def test_system_webhook_first_5xx(self):
        self.sys_wh.url = f"{self.url_base}/status/500"
        self.sys_wh.save()

        with self.assertLogs("dojo.notifications.helper", level="ERROR") as cm:
            manager = WebhookNotificationManger()
            manager.send_webhooks_notification(event="dummy", title="Dummy event")

        updated_wh = Notification_Webhooks.objects.filter(owner=None).first()
        self.assertEqual(updated_wh.status, Notification_Webhooks.Status.STATUS_INACTIVE_TMP)
        self.assertIsNotNone(updated_wh.first_error)
        self.assertEqual(updated_wh.first_error, updated_wh.last_error)
        self.assertEqual("Response status code: 500", updated_wh.note)
        self.assertIn("Error when sending message to Webhooks 'My webhook endpoint' (status: 500)", cm.output[-1])

    def test_system_webhook_second_5xx_within_one_day(self):
        ten_mins_ago = get_current_datetime() - datetime.timedelta(minutes=10)
        self.sys_wh.url = f"{self.url_base}/status/500"
        self.sys_wh.status = Notification_Webhooks.Status.STATUS_ACTIVE_TMP
        self.sys_wh.first_error = ten_mins_ago
        self.sys_wh.last_error = ten_mins_ago
        self.sys_wh.save()

        with self.assertLogs("dojo.notifications.helper", level="ERROR") as cm:
            manager = WebhookNotificationManger()
            manager.send_webhooks_notification(event="dummy", title="Dummy event")

        updated_wh = Notification_Webhooks.objects.filter(owner=None).first()
        self.assertEqual(updated_wh.status, Notification_Webhooks.Status.STATUS_INACTIVE_TMP)
        self.assertEqual(updated_wh.first_error, ten_mins_ago)
        self.assertGreater(updated_wh.last_error, ten_mins_ago)
        self.assertEqual("Response status code: 500", updated_wh.note)
        self.assertIn("Error when sending message to Webhooks 'My webhook endpoint' (status: 500)", cm.output[-1])

    def test_system_webhook_third_5xx_after_more_then_day(self):
        now = get_current_datetime()
        day_ago = now - datetime.timedelta(hours=24, minutes=10)
        ten_minutes_ago = now - datetime.timedelta(minutes=10)
        self.sys_wh.url = f"{self.url_base}/status/500"
        self.sys_wh.status = Notification_Webhooks.Status.STATUS_ACTIVE_TMP
        self.sys_wh.first_error = day_ago
        self.sys_wh.last_error = ten_minutes_ago
        self.sys_wh.save()

        with self.assertLogs("dojo.notifications.helper", level="ERROR") as cm:
            manager = WebhookNotificationManger()
            manager.send_webhooks_notification(event="dummy", title="Dummy event")

        updated_wh = Notification_Webhooks.objects.filter(owner=None).first()
        self.assertEqual(updated_wh.status, Notification_Webhooks.Status.STATUS_INACTIVE_PERMANENT)
        self.assertEqual(updated_wh.first_error, day_ago)
        self.assertGreater(updated_wh.last_error, ten_minutes_ago)
        self.assertEqual("Response status code: 500", updated_wh.note)
        self.assertIn("Error when sending message to Webhooks 'My webhook endpoint' (status: 500)", cm.output[-1])

    def test_webhook_reactivation(self):
        with self.subTest("active"):
            wh = Notification_Webhooks.objects.filter(owner=None).first()
            manager = WebhookNotificationManger()
            manager._webhook_reactivation(manager, endpoint_id=wh.pk)

            updated_wh = Notification_Webhooks.objects.filter(owner=None).first()
            self.assertEqual(updated_wh.status, Notification_Webhooks.Status.STATUS_ACTIVE)
            self.assertIsNone(updated_wh.first_error)
            self.assertIsNone(updated_wh.last_error)
            self.assertIsNone(updated_wh.note)

        with self.subTest("inactive"):
            now = get_current_datetime()
            wh = Notification_Webhooks.objects.filter(owner=None).first()
            wh.status = Notification_Webhooks.Status.STATUS_INACTIVE_TMP
            wh.first_error = now
            wh.last_error = now
            wh.note = "Response status code: 418"
            wh.save()

            with self.assertLogs("dojo.notifications.helper", level="DEBUG") as cm:
                manager = WebhookNotificationManger()
                manager._webhook_reactivation(manager, endpoint_id=wh.pk)

            updated_wh = Notification_Webhooks.objects.filter(owner=None).first()
            self.assertEqual(updated_wh.status, Notification_Webhooks.Status.STATUS_ACTIVE_TMP)
            self.assertIsNotNone(updated_wh.first_error)
            self.assertEqual(updated_wh.first_error, updated_wh.last_error)
            self.assertEqual(updated_wh.note, "Response status code: 418")
            self.assertIn("Webhook endpoint 'My webhook endpoint' reactivated to 'active_tmp'", cm.output[-1])

    def test_webhook_status_cleanup(self):
        with self.subTest("active"):
            webhook_status_cleanup()

            updated_wh = Notification_Webhooks.objects.filter(owner=None).first()
            self.assertEqual(updated_wh.status, Notification_Webhooks.Status.STATUS_ACTIVE)
            self.assertIsNone(updated_wh.first_error)
            self.assertIsNone(updated_wh.last_error)
            self.assertIsNone(updated_wh.note)

        with self.subTest("active_tmp_new"):
            now = get_current_datetime()
            wh = Notification_Webhooks.objects.filter(owner=None).first()
            wh.status = Notification_Webhooks.Status.STATUS_ACTIVE_TMP
            wh.first_error = now
            wh.last_error = now
            wh.note = "Response status code: 503"
            wh.save()

            webhook_status_cleanup()

            updated_wh = Notification_Webhooks.objects.filter(owner=None).first()
            self.assertEqual(updated_wh.status, Notification_Webhooks.Status.STATUS_ACTIVE_TMP)
            self.assertIsNotNone(updated_wh.first_error)
            self.assertEqual(updated_wh.first_error, updated_wh.last_error)
            self.assertEqual(updated_wh.note, "Response status code: 503")

        with self.subTest("active_tmp_old"):
            day_ago = get_current_datetime() - datetime.timedelta(hours=24, minutes=10)
            wh = Notification_Webhooks.objects.filter(owner=None).first()
            wh.status = Notification_Webhooks.Status.STATUS_ACTIVE_TMP
            wh.first_error = day_ago
            wh.last_error = day_ago
            wh.note = "Response status code: 503"
            wh.save()

            with self.assertLogs("dojo.notifications.helper", level="DEBUG") as cm:
                webhook_status_cleanup()

            updated_wh = Notification_Webhooks.objects.filter(owner=None).first()
            self.assertEqual(updated_wh.status, Notification_Webhooks.Status.STATUS_ACTIVE)
            self.assertIsNone(updated_wh.first_error)
            self.assertIsNone(updated_wh.last_error)
            self.assertEqual(updated_wh.note, "Reactivation from active_tmp")
            self.assertIn("Webhook endpoint 'My webhook endpoint' reactivated from 'active_tmp' to 'active'", cm.output[-1])

        with self.subTest("inactive_tmp_new"):
            minute_ago = get_current_datetime() - datetime.timedelta(minutes=1)
            wh = Notification_Webhooks.objects.filter(owner=None).first()
            wh.status = Notification_Webhooks.Status.STATUS_INACTIVE_TMP
            wh.first_error = minute_ago
            wh.last_error = minute_ago
            wh.note = "Response status code: 503"
            wh.save()

            webhook_status_cleanup()

            updated_wh = Notification_Webhooks.objects.filter(owner=None).first()
            self.assertEqual(updated_wh.status, Notification_Webhooks.Status.STATUS_INACTIVE_TMP)
            self.assertEqual(updated_wh.first_error, minute_ago)
            self.assertEqual(updated_wh.last_error, minute_ago)
            self.assertEqual(updated_wh.note, "Response status code: 503")

        with self.subTest("inactive_tmp_old"):
            ten_minutes_ago = get_current_datetime() - datetime.timedelta(minutes=10)
            wh = Notification_Webhooks.objects.filter(owner=None).first()
            wh.status = Notification_Webhooks.Status.STATUS_INACTIVE_TMP
            wh.first_error = ten_minutes_ago
            wh.last_error = ten_minutes_ago
            wh.note = "Response status code: 503"
            wh.save()

            with self.assertLogs("dojo.notifications.helper", level="DEBUG") as cm:
                webhook_status_cleanup()

            updated_wh = Notification_Webhooks.objects.filter(owner=None).first()
            self.assertEqual(updated_wh.status, Notification_Webhooks.Status.STATUS_ACTIVE_TMP)
            self.assertEqual(updated_wh.first_error, ten_minutes_ago)
            self.assertEqual(updated_wh.last_error, ten_minutes_ago)
            self.assertEqual(updated_wh.note, "Response status code: 503")
            self.assertIn("Webhook endpoint 'My webhook endpoint' reactivated to 'active_tmp'", cm.output[-1])

    def test_system_webhook_timeout(self):
        self.sys_wh.url = f"{self.url_base}/delay/3"
        self.sys_wh.save()

        system_settings = System_Settings.objects.get()
        system_settings.webhooks_notifications_timeout = 1
        system_settings.save()

        with self.assertLogs("dojo.notifications.helper", level="ERROR") as cm:
            manager = WebhookNotificationManger()
            manager.send_webhooks_notification(event="dummy", title="Dummy event")

        updated_wh = Notification_Webhooks.objects.filter(owner=None).first()
        self.assertEqual(updated_wh.status, Notification_Webhooks.Status.STATUS_INACTIVE_TMP)
        self.assertIsNotNone(updated_wh.first_error)
        self.assertEqual(updated_wh.first_error, updated_wh.last_error)
        self.assertIn("HTTPConnectionPool(host='webhook.endpoint', port=8080): Read timed out.", updated_wh.note)
        self.assertIn("Timeout when sending message to Webhook 'My webhook endpoint'", cm.output[-1])

    def test_system_webhook_wrong_fqdn(self):

        self.sys_wh.url = "http://non.existing.place"
        self.sys_wh.save()

        with self.assertLogs("dojo.notifications.helper", level="ERROR") as cm:
            manager = WebhookNotificationManger()
            manager.send_webhooks_notification(event="dummy", title="Dummy event")

        updated_wh = Notification_Webhooks.objects.filter(owner=None).first()
        self.assertEqual(updated_wh.status, Notification_Webhooks.Status.STATUS_INACTIVE_PERMANENT)
        self.assertIsNotNone(updated_wh.first_error)
        self.assertEqual(updated_wh.first_error, updated_wh.last_error)
        self.assertIn("HTTPConnectionPool(host='non.existing.place', port=80): Max retries exceeded with url: /", updated_wh.note)
        self.assertIn("HTTPConnectionPool(host='non.existing.place', port=80): Max retries exceeded with url: /", cm.output[-1])

    @patch("requests.request", **{"return_value.status_code": 200})
    def test_headers(self, mock):
        Product_Type.objects.create(name="notif prod type")
        self.assertEqual(mock.call_args.kwargs["headers"], {
            "User-Agent": f"DefectDojo-{dd_version}",
            "X-DefectDojo-Event": "product_type_added",
            "X-DefectDojo-Instance": "http://localhost:8080",
            "Accept": "application/json",
            "Auth": "Token xxx",
        })

    @patch("requests.request", **{"return_value.status_code": 200})
    def test_events_messages(self, mock):
        with self.subTest("ping"):
            manager = WebhookNotificationManger()
            manager._test_webhooks_notification(self.sys_wh)
            self.assertEqual(mock.call_args.kwargs["headers"]["X-DefectDojo-Event"], "ping")
            self.maxDiff = None
            self.assertEqual(mock.call_args.kwargs["json"], {
                "description": "Test webhook notification",
                "title": "",
                "user": None,
            })

        with self.subTest("product_type_added"):
            prod_type = Product_Type.objects.create(name="notif prod type")
            self.assertEqual(mock.call_args.kwargs["headers"]["X-DefectDojo-Event"], "product_type_added")
            self.maxDiff = None
            self.assertEqual(mock.call_args.kwargs["json"], {
                "description": "Product Type notif prod type has been created successfully.",
                "title": "notif prod type",
                "user": None,
                "url_api": f"http://localhost:8080/api/v2/product_types/{prod_type.pk}/",
                "url_ui": f"http://localhost:8080/product/type/{prod_type.pk}",
                "product_type": {
                    "id": prod_type.pk,
                    "name": "notif prod type",
                    "url_api": f"http://localhost:8080/api/v2/product_types/{prod_type.pk}/",
                    "url_ui": f"http://localhost:8080/product/type/{prod_type.pk}",
                },
            })

        with self.subTest("product_added"):
            prod = Product.objects.create(name="notif prod", prod_type=prod_type)
            self.assertEqual(mock.call_args.kwargs["headers"]["X-DefectDojo-Event"], "product_added")
            self.maxDiff = None
            self.assertEqual(mock.call_args.kwargs["json"], {
                "description": "Product notif prod has been created successfully.",
                "title": "notif prod",
                "user": None,
                "url_api": f"http://localhost:8080/api/v2/products/{prod.pk}/",
                "url_ui": f"http://localhost:8080/product/{prod.pk}",
                "product_type": {
                    "id": prod_type.pk,
                    "name": "notif prod type",
                    "url_api": f"http://localhost:8080/api/v2/product_types/{prod_type.pk}/",
                    "url_ui": f"http://localhost:8080/product/type/{prod_type.pk}",
                },
                "product": {
                    "id": prod.pk,
                    "name": "notif prod",
                    "url_api": f"http://localhost:8080/api/v2/products/{prod.pk}/",
                    "url_ui": f"http://localhost:8080/product/{prod.pk}",
                },
            })

        with self.subTest("engagement_added"):
            eng = Engagement.objects.create(name="notif eng", product=prod, target_start=timezone.now(), target_end=timezone.now())
            self.assertEqual(mock.call_args.kwargs["headers"]["X-DefectDojo-Event"], "engagement_added")
            self.maxDiff = None
            self.assertEqual(mock.call_args.kwargs["json"], {
                "description": "Event engagement_added has occurred.",
                "title": 'Engagement created for "notif prod": notif eng',
                "user": None,
                "url_api": f"http://localhost:8080/api/v2/engagements/{eng.pk}/",
                "url_ui": f"http://localhost:8080/engagement/{eng.pk}",
                "product_type": {
                    "id": prod_type.pk,
                    "name": "notif prod type",
                    "url_api": f"http://localhost:8080/api/v2/product_types/{prod_type.pk}/",
                    "url_ui": f"http://localhost:8080/product/type/{prod_type.pk}",
                },
                "product": {
                    "id": prod.pk,
                    "name": "notif prod",
                    "url_api": f"http://localhost:8080/api/v2/products/{prod.pk}/",
                    "url_ui": f"http://localhost:8080/product/{prod.pk}",
                },
                "engagement": {
                    "id": eng.pk,
                    "name": "notif eng",
                    "url_api": f"http://localhost:8080/api/v2/engagements/{eng.pk}/",
                    "url_ui": f"http://localhost:8080/engagement/{eng.pk}",
                },
            })

        with self.subTest("test_added"):
            test = Test.objects.create(title="notif test", engagement=eng, target_start=timezone.now(), target_end=timezone.now(), test_type_id=Test_Type.objects.first().id)
            create_notification(
                event="test_added",
                title=f"Test created for {test.engagement.product}: {test.engagement.name}: {test}",
                test=test,
                engagement=test.engagement,
                product=test.engagement.product,
                url=reverse("view_test", args=(test.id,)),
                url_api=reverse("test-detail", args=(test.id,)),
            )
            self.assertEqual(mock.call_args.kwargs["headers"]["X-DefectDojo-Event"], "test_added")
            self.maxDiff = None
            self.assertEqual(mock.call_args.kwargs["json"], {
                "description": "Event test_added has occurred.",
                "title": "Test created for notif prod: notif eng: notif test (Acunetix Scan)",
                "user": None,
                "url_api": f"http://localhost:8080/api/v2/tests/{test.pk}/",
                "url_ui": f"http://localhost:8080/test/{test.pk}",
                "product_type": {
                    "id": prod_type.pk,
                    "name": "notif prod type",
                    "url_api": f"http://localhost:8080/api/v2/product_types/{prod_type.pk}/",
                    "url_ui": f"http://localhost:8080/product/type/{prod_type.pk}",
                },
                "product": {
                    "id": prod.pk,
                    "name": "notif prod",
                    "url_api": f"http://localhost:8080/api/v2/products/{prod.pk}/",
                    "url_ui": f"http://localhost:8080/product/{prod.pk}",
                },
                "engagement": {
                    "id": eng.pk,
                    "name": "notif eng",
                    "url_api": f"http://localhost:8080/api/v2/engagements/{eng.pk}/",
                    "url_ui": f"http://localhost:8080/engagement/{eng.pk}",
                },
                "test": {
                    "id": test.pk,
                    "title": "notif test",
                    "url_api": f"http://localhost:8080/api/v2/tests/{test.pk}/",
                    "url_ui": f"http://localhost:8080/test/{test.pk}",
                },
            })

        with self.subTest("scan_added_empty"):
            BaseImporter(
                environment=Development_Environment.objects.get_or_create(name="Development")[0],
                scan_type="ZAP Scan",
            ).notify_scan_added(test, updated_count=0)
            self.assertEqual(mock.call_args.kwargs["headers"]["X-DefectDojo-Event"], "scan_added_empty")
            self.maxDiff = 99999
            self.assertEqual(mock.call_args.kwargs["json"], {
                "description": "Event scan_added_empty has occurred.",
                "title": "Created/Updated 0 findings for notif prod: notif eng: notif test (Acunetix Scan)",
                "user": None,
                "url_api": f"http://localhost:8080/api/v2/tests/{test.pk}/",
                "url_ui": f"http://localhost:8080/test/{test.pk}",
                "product_type": {
                    "id": prod_type.pk,
                    "name": "notif prod type",
                    "url_api": f"http://localhost:8080/api/v2/product_types/{prod_type.pk}/",
                    "url_ui": f"http://localhost:8080/product/type/{prod_type.pk}",
                },
                "product": {
                    "id": prod.pk,
                    "name": "notif prod",
                    "url_api": f"http://localhost:8080/api/v2/products/{prod.pk}/",
                    "url_ui": f"http://localhost:8080/product/{prod.pk}",
                },
                "engagement": {
                    "id": eng.pk,
                    "name": "notif eng",
                    "url_api": f"http://localhost:8080/api/v2/engagements/{eng.pk}/",
                    "url_ui": f"http://localhost:8080/engagement/{eng.pk}",
                },
                "test": {
                    "id": test.pk,
                    "title": "notif test",
                    "url_api": f"http://localhost:8080/api/v2/tests/{test.pk}/",
                    "url_ui": f"http://localhost:8080/test/{test.pk}",
                },
                "finding_count": 0,
                "findings": {
                    "mitigated": [],
                    "new": [],
                    "reactivated": [],
                    "untouched": [],
                },
            })

        with self.subTest("scan_added"):
            new_finding = Finding.objects.create(test=test, title="New Finding", severity="Critical")
            mitigated_finding = Finding.objects.create(test=test, title="Mitigated Finding", severity="Medium")
            reactivated_finding = Finding.objects.create(test=test, title="Reactivated Finding", severity="Low")
            untouched_finding = Finding.objects.create(test=test, title="Untouched Finding", severity="Info")

            BaseImporter(
                environment=Development_Environment.objects.get_or_create(name="Development")[0],
                scan_type="ZAP Scan",
            ).notify_scan_added(
                test,
                updated_count=4,
                new_findings=[
                    new_finding,
                ],
                findings_mitigated=[
                    mitigated_finding,
                ],
                findings_reactivated=[
                    reactivated_finding,
                ],
                findings_untouched=[
                    untouched_finding,
                ],
            )
            self.assertEqual(mock.call_args.kwargs["headers"]["X-DefectDojo-Event"], "scan_added")
            self.maxDiff = None
            self.assertEqual(mock.call_args.kwargs["json"]["findings"], {
                "new": [{
                    "id": new_finding.id,
                    "title": "New Finding",
                    "severity": "Critical",
                    "url_api": f"http://localhost:8080/api/v2/findings/{new_finding.id}/",
                    "url_ui": f"http://localhost:8080/finding/{new_finding.id}",
                }],
                "mitigated": [{
                    "id": mitigated_finding.id,
                    "title": "Mitigated Finding",
                    "severity": "Medium",
                    "url_api": f"http://localhost:8080/api/v2/findings/{mitigated_finding.id}/",
                    "url_ui": f"http://localhost:8080/finding/{mitigated_finding.id}",
                }],
                "reactivated": [{
                    "id": reactivated_finding.id,
                    "title": "Reactivated Finding",
                    "severity": "Low",
                    "url_api": f"http://localhost:8080/api/v2/findings/{reactivated_finding.id}/",
                    "url_ui": f"http://localhost:8080/finding/{reactivated_finding.id}",
                }],
                "untouched": [{
                    "id": untouched_finding.id,
                    "title": "Untouched Finding",
                    "severity": "Info",
                    "url_api": f"http://localhost:8080/api/v2/findings/{untouched_finding.id}/",
                    "url_ui": f"http://localhost:8080/finding/{untouched_finding.id}",
                }],
            })

        with self.subTest("scan_added problematic titles"):
            colon_new_finding = Finding.objects.create(test=test, title="Colon: New Finding", severity="Critical")
            brackets_mitigated_finding = Finding.objects.create(test=test, title="[Brackets] Mitigated Finding", severity="Medium")
            quotation1_reactivated_finding = Finding.objects.create(test=test, title='"Quotation1" Reactivated Finding', severity="Low")
            quotation2_untouched_finding = Finding.objects.create(test=test, title="'Quotation2' Untouched Finding", severity="Info")

            BaseImporter(
                environment=Development_Environment.objects.get_or_create(name="Development")[0],
                scan_type="ZAP Scan",
            ).notify_scan_added(
                test,
                updated_count=4,
                new_findings=[
                    colon_new_finding,
                ],
                findings_mitigated=[
                    brackets_mitigated_finding,
                ],
                findings_reactivated=[
                    quotation1_reactivated_finding,
                ],
                findings_untouched=[
                    quotation2_untouched_finding,
                ],
            )
            self.assertEqual(mock.call_args.kwargs["headers"]["X-DefectDojo-Event"], "scan_added")
            self.maxDiff = None
            self.assertEqual(mock.call_args.kwargs["json"]["findings"], {
                "new": [{
                    "id": colon_new_finding.id,
                    "title": "Colon: New Finding",
                    "severity": "Critical",
                    "url_api": f"http://localhost:8080/api/v2/findings/{colon_new_finding.id}/",
                    "url_ui": f"http://localhost:8080/finding/{colon_new_finding.id}",
                }],
                "mitigated": [{
                    "id": brackets_mitigated_finding.id,
                    "title": "[Brackets] Mitigated Finding",
                    "severity": "Medium",
                    "url_api": f"http://localhost:8080/api/v2/findings/{brackets_mitigated_finding.id}/",
                    "url_ui": f"http://localhost:8080/finding/{brackets_mitigated_finding.id}",
                }],
                "reactivated": [{
                    "id": quotation1_reactivated_finding.id,
                    "title": '"Quotation1" Reactivated Finding',
                    "severity": "Low",
                    "url_api": f"http://localhost:8080/api/v2/findings/{quotation1_reactivated_finding.id}/",
                    "url_ui": f"http://localhost:8080/finding/{quotation1_reactivated_finding.id}",
                }],
                "untouched": [{
                    "id": quotation2_untouched_finding.id,
                    "title": "'Quotation2' Untouched Finding",
                    "severity": "Info",
                    "url_api": f"http://localhost:8080/api/v2/findings/{quotation2_untouched_finding.id}/",
                    "url_ui": f"http://localhost:8080/finding/{quotation2_untouched_finding.id}",
                }],
            })

    @patch("requests.request", **{"return_value.status_code": 200})
    def test_ping_with_owner_assigned(self, mock):
        """
        We only need to test one event because the user is serialized in the base sub template. This allows to]
        assert that if the test passes for one event, it will pass for all events.
        """
        manager = WebhookNotificationManger()
        manager._test_webhooks_notification(Notification_Webhooks.objects.filter(owner__isnull=False).first())
        self.assertEqual(mock.call_args.kwargs["headers"]["X-DefectDojo-Event"], "ping")
        self.maxDiff = None
        self.assertEqual(
            mock.call_args.kwargs["json"],
            {
                "description": "Test webhook notification",
                "title": "",
                "user": {
                    "id": 6,
                    "username": "user5",
                    "first_name": "User",
                    "last_name": "Five",
                    "email": "user5@email.com",
                    "url_api": "http://localhost:8080/api/v2/users/6/",
                    "url_ui": "http://localhost:8080/user/6",
                },
            },
        )
